Skip to content

S11-08 Vue-进阶

[TOC]

自定义指令

自定义指令

在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。

  • 注意:在Vue中,代码的复用和抽象主要还是通过组件;

  • 通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;

自定义指令分为两种:

  • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;

  • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用;

比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点

  • 实现方式一:如果我们使用默认的实现方式;

  • 实现方式二:自定义一个 v-focus 的局部指令;

  • 实现方式三:自定义一个 v-focus 的全局指令;

实现方式一:聚焦的默认实现

image-20250213165426143

实现方式二:局部自定义指令

实现方式二:自定义一个 v-focus 的局部指令

  • 这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;

  • 它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);

  • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;

image-20250213165438057

方式三:自定义全局指令

自定义一个全局的v-focus指令可以让我们在任何地方直接使用

image-20250213165448580

指令的生命周期

一个指令定义的对象,Vue提供了如下的几个钩子函数:

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用;

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;

  • mounted:在绑定元素的父组件被挂载后调用;

  • beforeUpdate:在更新包含组件的 VNode 之前调用;

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;

  • beforeUnmount:在卸载绑定元素的父组件之前调用;

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;

指令的参数和修饰符

如果我们指令需要接受一些参数或者修饰符应该如何操作呢?

  • info是参数的名称;

  • aaa-bbb是修饰符的名称;

  • 后面是传入的具体的值;

在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:

image-20250213165505869

image-20250213165513892

案例:时间格式化指令

自定义指令案例:时间戳的显示需求:

  • 在开发中,大多数情况下从服务器获取到的都是时间戳;

  • 我们需要将时间戳转换成具体格式化的时间来展示;

  • 在Vue2中我们可以通过过滤器来完成;

  • 在Vue3中我们可以通过 计算属性(computed) 或者 自定义一个方法(methods) 来完成;

  • 其实我们还可以通过一个自定义的指令来完成;

我们来实现一个可以自动对时间格式化的指令v-format-time:

  • 这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可;

代码

image-20250213165526017

高阶组件

<Teleport>

认识Teleport

在组件化开发中,我们封装一个组件A,在另外一个组件B中使用:

  • 那么组件A中template的元素,会被挂载到组件B中template的某个位置;

  • 最终我们的应用程序会形成一颗DOM树结构;

但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置:

  • 比如移动到body元素上,或者我们有其他的div#app之外的元素上;

  • 这个时候我们就可以通过teleport来完成;

Teleport是什么呢?

  • 它是一个Vue提供的内置组件,类似于react的Portals;

  • teleport翻译过来是心灵传输、远距离运输的意思;

    • 它有两个属性:
      • to:指定将其中的内容移动到的目标元素,可以使用选择器;
      • disabled:是否禁用 teleport 的功能;

代码的效果

image-20250213165601223

image-20250213165609208

结合组件使用

当然,teleport也可以和组件结合一起来使用:

  • 我们可以在 teleport 中使用组件,并且也可以给他传入一些数据;

image-20250213165621634

image-20250213165627779

多个teleport

如果我们将多个teleport应用到同一个目标上(to的值相同),那么这些目标会进行合并:

image-20250213165635385

实现效果如下:

image-20250213165641966

<Suspense>

异步组件和Suspense

注意:目前(2022-08-01)Suspense显示的是一个实验性的特性,API随时可能会修改。

Suspense是一个内置的全局组件,该组件有两个插槽:

  • default:如果default可以显示,那么显示default的内容;

  • fallback:如果default无法显示,那么会显示fallback插槽的内容;

image-20250213165656602

自定义插件

认识Vue插件

通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:

  • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;

  • 函数类型:一个function,这个函数会在安装插件时自动执行;

插件可以完成的功能没有限制,比如下面的几种都是可以的:

  • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;

  • 添加全局资源:指令/过滤器/过渡等;

  • 通过全局 mixin 来添加一些组件选项;

  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;

插件的编写方式

对象类型的写法

image-20250213165710216

函数类型的写法

image-20250213165717116

h函数

h函数

Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;

前面我们讲解过VNode和VDOM的概念:

  • Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM (VDOM);

  • 事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;

  • 那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;

那么我们应该怎么来做呢?使用 h()函数:

  • h() 函数是一个用于创建 vnode 的一个函数;

  • 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;

基本使用

h()函数 如何使用呢?它接受三个参数:

image-20250213165738573

image-20250213165746029

image-20250213165752703

注意事项

  • 如果没有props,那么通常可以将children作为第二个参数传入;

  • 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;

h函数可以在两个地方使用:

  • render函数选项中;

  • setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);

image-20250213165810557

image-20250213165816423

案例:计数器

image-20250213165825482

JSX

babel配置

如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持:

  • jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);

  • 对于Vue来说,我们只需要在Babel中配置对应的插件即可;

安装Babel支持Vue的jsx插件:npm install @vue/babel-plugin-jsx -D

在babel.config.js配置文件中配置插件:

image-20250213165904273

image-20250213165909977

如果是Vite环境,需要安装插件:npm install @vitejs/plugin-vue-jsx -D

案例:计数器

image-20250213165920050

响应式原理

响应式

案例:普通值的响应式:我们先来看一下响应式意味着什么?我们来看一段代码:

  • num有一个初始化的值,有一段代码使用了这个值;

  • 那么在num有一个新的值时,这段代码可以自动重新执行;

    image-20250213171355894

响应式:上面的这样一种可以自动响应数据变化的代码机制,我们就称之为是响应式的。


案例:对象的响应式:那么我们再来看一下对象的响应式。

image-20250213171942408

image-20250213164438330

响应式实现

实现-响应式函数

设计

响应式函数:执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中。问题就变成了当数据发生变化时,自动去执行某个函数。

image-20250213172415551

image-20250213164455889

问题:在开发中我们是有很多的函数的,我们如何区分一个函数是否需要响应式

  • 下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应。

    image-20250213172917533

  • bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作。

    image-20250213172843962

手动收集

思路:区分一个函数是否需要响应式:手动收集响应式函数。

  • 封装一个新的函数watchFn;
  • 凡是传入到watchFn的函数,就是需要响应式的;
  • 其他默认定义的函数都是不需要响应式的;

代码实现

1、封装专门手动收集响应式函数的函数watchFn(),收集后立即执行一次函数。

image-20250213174416421

2、调用watchFn()函数,传入响应式的函数foo、bar。

image-20250213173843496

3、当属性变化时,遍历执行收集的函数。

image-20250213174026807

封装-Depend

需求:监听多对象的响应式

image-20250217164659957

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

  • 我们在实际开发中需要监听很多对象的响应式;
  • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
  • 我们不可能在全局维护一大堆的数组来保存这些响应函数;

所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:

封装Depend类:替代原来简单的 reactiveFns 数组

1、将上述的逻辑封装到一个Depend类。

image-20250213175339931

2、调用类中的addDepend()方法收集依赖。

image-20250217144942654

3、调用watchFn()函数,传入响应式的函数foo、bar。

image-20250213173843496

4、数据变化后,手动调用notify()方法重新运行所有收集到的函数。

image-20250213175516307

实现-监听对象变化

image-20250213175516307

需求:监听对象变化:替代之前使用dep.notify()手动运行响应式函数。

  • Vue2:通过Object.defineProperty()的方式监听
  • Vue3:通过new Proxy()的方式监听

代码实现:

Vue2:通过Object.defineProperty()的方式监听

image-20250213224336440

Vue3:通过new Proxy()的方式监听

image-20250213164637720

实现-自动收集依赖

对象依赖管理

image-20250217165012672

需求:管理多对象的多属性依赖

我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:

  • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
  • 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?

思路图解:在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖。

  • 1、dep对象数据结构的管理
    • 每一个对象的每一个属性都会对应一个dep对象
    • 同一个对象的多个属性的dep对象存放在一个map对象中
    • 多个对象的map对象,存放在一个objMap对象(弱引用)中
  • 2、依赖收集:当执行get函数时,自动添加fn函数

image-20250213164659284

对象依赖管理-实现

我们可以写一个getDepend()函数专门来管理这种依赖关系:

1、通过Object.defineProperty()函数的setter方法监听对象的修改,触发时回调setter方法。在其中生成dep实例,并返回保存到对象Map.get(属性)的Map中的dep实例,对象修改时通过dep.notify()方法重新运行收集到的响应函数。

image-20250217165448154

2、封装 getDepend() 函数,负责通过obj的key生成/获取对应的Depend实例。

image-20250217164436324

3、当对象属性变化时,会自动运行相关的响应函数。

image-20250217170609444

5、

image-20250213164716210

image-20250213164722567

对象的依赖收集

问题:我们之前收集依赖的地方是在 watchFn 中,但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖,只能针对一个单独的depend对象来添加你的依赖对象。

思路:应该在调用Proxy或Object.defineProperty的get捕获器中收集依赖函数。

代码实现

1、在watchFn()方法中将监听的函数保存到全局变量reactiveFn中。

image-20250217170118347

2、在getter中访问到相关对象的属性时,将其添加到其对应的dep实例中。

image-20250217170207938

3、当执行watchFn()函数时,会在它的回调中访问到对象的属性,我们需要将这些属性自动收集起来。

image-20250217163725555

细节补充

问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次

image-20250217172542320

思路:不使用数组,而是使用Set保存收集的响应函数。

代码实现

image-20250217173353970


问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为。

思路:添加一个新的方法,用于收集依赖。

代码实现

1、去除defineProperty()中对外部变量的依赖。

image-20250217173457027

2、将对外部变量的依赖放入Depend类的depend方法中。

image-20250217173631736

多个对象响应式

问题:目前的响应式是针对于obj一个对象的,其他对象无法实现响应式。

image-20250217174409663

思路:封装reactive()函数,针对所有的对象都可以变成响应式对象。

代码实现

1、封装reactive()函数。

image-20250217174943819

2、使用reactive()函数创建一个响应式对象。

image-20250217174920970

3、通过watchFn()函数,收集依赖。

image-20250217175240048

4、当对象变化时重新执行依赖该对象的函数。

image-20250217175339204

重构-Vue3 Proxy

前面所实现的响应式的代码是Vue2的响应式原理。Vue3主要通过 Proxy 来监听数据的变化以及收集相关的依赖。

代码实现:Proxy + Reflect

image-20250217181008509